#include "Controller.h"

#include "../logger/ScopedWriter.h"

#define _(x) QString::fromUtf8(x)

#define MAX_SEEDS 4

#include <iostream>

namespace Client {


/*

*/

SearchResultModel::SearchResultModel(QList<FileDesc> & ref, QObject * parent) : QAbstractItemModel(parent), search_(ref) {

}

int SearchResultModel::rowCount(const QModelIndex & parent) const {
	Q_UNUSED(parent)
	return search_.size();
}

int SearchResultModel::columnCount(const QModelIndex & parent) const {
	Q_UNUSED(parent)
	// nazwa, typ, rozmiar, ilość pobrań, hash
	return 5;
}

QVariant SearchResultModel::data(const QModelIndex & index, int role) const {
	FileDesc * file = static_cast<FileDesc*>(index.internalPointer());

	if (role != Qt::DisplayRole)
		 return QVariant();

	switch(index.column()) {
		case 0:
			return file->fname;
		case 1:
			return (int)file->type;
		case 2:
			return file->size;
		case 3:
			return 0;
		case 4:
			return file->hash.toHex();
		default:
			return QString("Hmmm");
	}
}

QModelIndex SearchResultModel::index(int row, int column, const QModelIndex &parent) const
{
	if (!hasIndex(row, column, parent))
		return QModelIndex();

	 if (parent.isValid())
		 return QModelIndex();

	FileDesc * file = &search_[row];

	return createIndex(row, column, file);
}

QModelIndex SearchResultModel::parent(const QModelIndex &index) const {
	Q_UNUSED(index)
	return QModelIndex();
}

Qt::ItemFlags SearchResultModel::flags(const QModelIndex &index) const
{
	if (!index.isValid())
		return 0;

	return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}

QVariant SearchResultModel::headerData(int section, Qt::Orientation orientation, int role) const
{
	if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
		switch (section) {
			case 0:
				return _("Nazwa pliku");
			case 1:
				return _("Typ");
			case 2:
				return _("Rozmiar");
			case 3:
				return _("Ilość pobrań");
			case 4:
				return _("MD5");
			default:
				return "";
		}
	}

	return QVariant();
}

/*

*/

Controller::Controller() : ncontroller(0) {
	main_server = NULL;
	ncontroller.start();

	sharedFilesModel = new SharedFilesModel(model.shared());
	searchResultModel = new SearchResultModel(search_);
	jobsModel = new JobsModel(model.jobs());


	connect(&ncontroller, SIGNAL(clientDied(RemoteClient *)), this, SLOT(onClientDied(RemoteClient *)));
	connect(&ncontroller, SIGNAL(newPacket(RemoteClient *)),  this, SLOT(onNewPacket(RemoteClient*)));
}


Controller::~Controller() {
	delete sharedFilesModel;
	delete searchResultModel;
	delete jobsModel;
}

void Controller::reconnect() {
	main_server = NULL;
	start(shost, sport);
}

bool Controller::start(QString host, int port) {

	ScopedWriter sw ("Controller::start()");

	if (main_server != NULL)
		return true;

	shost = host;
	sport = port;

	// łączymy się z serwerem
	main_server = ncontroller.connectToHost(host, port);

	// jeśli połączenie się nie udało
	if (main_server == NULL) {
		QTimer::singleShot(10*1000, this, SLOT(reconnect()));
		sw << "Próba połączenia z serwerem " << host.toStdString() << " nieudana. Kolejna za 10 sekund.\n";
		return false;
	}

	sw << "Połączono z serwerem " << main_server->getAddr().toStdString() << "\n";

	// jeśli jeszcze nie posiadamy id to trzeba się zarejestrować
	if (model.id() == 0) {
		sw << "Nie mam jeszcze id :-(\n";
		Packet * packet = new Packet;
		packet->id = Packet::ptIDRequest;
		main_server->send(packet);
	} else {
	// jeśli już posiadamy id to po prostu się logujemy
		sw << "Moje ID: " << model.id() << "\n";
		Packet * packet = new Packet;
		packet->id = Packet::ptLogin;
		packet->client_id = model.id();
		*packet << ncontroller.listeningAtPort();
		main_server->send(packet);
	}

	emit serverConnected();
	return true;
}

void Controller::stop() {
	ncontroller.stop();
	ncontroller.wait(3000);

	model.saveAll();
}

void Controller::find(QString query, Common::FileType type, int size_min, int size_max) {
	if (main_server) {
		Packet * packet = new Packet();
		packet->id = Packet::ptFind;
		packet->client_id = model.id();

		*packet << query << (int)type << size_min << size_max;

		main_server->send(packet);
	}
}

void Controller::download(FileDesc * file) {

	model.addJob(file->fname, file->hash, file->size, file->type);
	QTimer::singleShot(100, this, SLOT(handleJobs()));


	if (main_server) {
		// wysyłamy pakiet z pytaniem o aktywnych dawców
		Packet * packet = new Packet;
		packet->id = Packet::ptFindSeeds;
		packet->client_id = model.id();

		*packet << file->hash << file->size;

		main_server->send(packet);
	}
}

void Controller::addSharedFile(QString fname) {
	SharedFileDesc * fd = model.addSharedFile(fname);
	shared_.push_back(fd);
}

void Controller::sendAddedFiles() {
	if (shared_.size() == 0)
		return;


	if (main_server) {

		Packet * packet = new Packet;

		packet->id = Packet::ptFileList;
		packet->client_id = model.id();

		*packet << shared_.size();

		foreach (SharedFileDesc * file, shared_) {

			*packet << (int)Common::fsNew;
			*packet << file->fname;
			*packet << file->hash;
			*packet << file->size;
			*packet << (int)file->type;
		}

		shared_.clear();
		main_server->send(packet);
	}
}

RemoteClient * Controller::mainServer() {
        return main_server;
}

Packet * Controller::get(Packet * packet) {
	ScopedWriter sw("Client::Controller::get()", true);
	Packet * reply = new Packet;

	int chunk_number;
	QByteArray hash;
	int size;

	*packet >> hash >> size >> chunk_number;

	sw << "Ktoś chce fragment pliku..." << hash.toHex().data() << ":" << chunk_number << "\n";

	reply->id = Packet::ptGive;
	reply->client_id = model.id();

	*reply << hash << size << chunk_number;

	QByteArray ba = model.getChunk(hash, size, chunk_number);

	*reply << ba << Utils::md5sum(ba);

	return reply;
}

void Controller::give(Packet * packet) {
	ScopedWriter sw("Client::Controller::give()", true);

	int chunk_nr;
	QByteArray hash;
	int size;
	QByteArray chunk;
	QByteArray checksum;

	if (packet == NULL)
		return;

	if (packet->innerSize()+12 != packet->size) {
		sw << "Coś się zrypało. Odrzucam pakiet.\n";
		model.freeSeed(hash, size, packet->client_id);
		return;
	}

	try {
		*packet >> hash >> size >> chunk_nr;
		*packet >> chunk;
		*packet >> checksum;
	} catch(...)  {
		sw << "Coś się zrypało. Odrzucam pakiet.\n";
		model.freeSeed(hash, size, packet->client_id);
		return;
	}


	// sprawdzenie sumy kontrolnej pobranego fragmentu
	if (checksum != Utils::md5sum(chunk)) {
		sw << "Suma kontrolna pobranego fragmentu się nie zgadza. Pakiet odrzucony.\n";
		model.freeSeed(hash, size, packet->client_id);
		return;
	}

	//sw << "Przypałętał się nowy fragment pliku..." << hash.toHex().data() << ":" << chunk_nr << "\n";

	model.setChunk(hash, size, chunk_nr, chunk, packet->client_id);
}

void Controller::newID(Packet * packet) {
	int new_id;
	*packet >> new_id;
	model.id(new_id);
	std::cout << "Nadano nowe ID klienta: " << new_id << "\n";
	std::cout.flush();

	// jak już dostaliśmy nasze nowe boskie id to warto się zalogować
	Packet * reply = new Packet;
	reply->id = Packet::ptLogin;
	reply->client_id = model.id();
	*reply << ncontroller.listeningAtPort();
	main_server->send(reply);
}

void Controller::list(Packet * packet) {
	int count;

	search_.clear();

	*packet >> count;

	for (int i = 0; i < count; ++i) {
		FileDesc fd;
		*packet >> fd.fname;
		int t;
		*packet >> t;
		fd.type = (Common::FileType)t;
		*packet >> fd.size;
		*packet >> fd.hash;

		search_.push_back(fd);
	}

	emit updateSearchResult();
}

void Controller::listSeeds(Packet * packet) {
	ScopedWriter sw("Client::Controller::listSeeds()", false);

	int count;
	PeerInfo peer;
	QByteArray hash;
	int size;

	*packet >> hash >> size;

	// znajdujemy robotkę związaną z pakietem który dostaliśli
	// no bo skoro nie ma robotki, to nie ma gdzie dodawać :-)
	Job * job = NULL;

	foreach (Job * j, model.jobs()) {
		if ( (j->hash == hash) && (j->size == size) ) {
			job = j;
			break;
		}
	}

	if (job == NULL)
		return;

	sw << "Znaleziono robotę...\n";

	*packet >> count;

	sw << "Dodaję " << count << " dawców\n";

	for (int i = 0; i < count; ++i) {
		*packet >> peer.id;
		*packet >> peer.ip;
		*packet >> peer.port;
		//job->seeds.push_back(peer);
		job->addSeed(peer);
	}
}

void Controller::handleJobs() {
	ScopedWriter sw("Client::Controller::handleJobs()", true);
	int active = 0;

	foreach (Job * job, model.jobs()) {

		// zadanie aktywne
		if (( job->status == jsActive) || (job->status == jsPending)) {

			// jeśli zadanie jest już stare, to pobieramy mu nową listę seedów
			if (job->age++ > 20) {
				if (main_server) {
					job->age = 0;

					Packet * packet = new Packet;
					packet->id = Packet::ptFindSeeds;
					packet->client_id = model.id();

					*packet << job->hash << job->size;

					main_server->send(packet);
				}
			}

			active++;

			//sw << "Aktywne zadanie: " << job->fname.toStdString() << "[pobrano " << job->chunks_done << "/" << job->chunks_count << "\n";

			// jeśli nadal mamy wolny slot
			if (job->seeds_count < MAX_SEEDS) {

				//sw << "  szukamy dawcy..\n";
				// szukamy dawcy, od którego jeszcze nic nie bierzemy
				foreach (PeerInfo peer, job->seeds) {
					sw << "  :: " << peer.id << "@" << peer.ip << ":" << peer.port << "\n";

					if (!peer.connected) {

						RemoteClient * client = ncontroller.getOutgoingById(peer.id);
						if (client == NULL) {
							sw << " Brak połączenia. Łączymy...";
							client = ncontroller.connectToHostByIp(QHostAddress(peer.ip), peer.port, peer.id);
						}
						// jeśli nie udało nam się nawiązać połączenia z tym dawcą
						// to wyrzucamy go na koniec kolejki
						if (client == NULL) {
							sw << " nie udało się :-(\n";
							/*job->seeds.removeOne(peer);
							job->seeds.push_back(peer);*/
							continue;
						}

						sw << " połączony! [" << client->getAddr().toStdString() << "]\n";
						job->seeds_count++;
						int ind = job->seeds.indexOf(peer);
						job->seeds[ind].connected = true;

						int next_chunk = job->getNextFreeChunk(true);
						if (next_chunk >= 0) {
							// przygotowujemy pakiet z prośbą o fragment pliku
							Packet * packet = new Packet;
							packet->id = Packet::ptGet;
							packet->client_id = model.id();

							*packet << job->hash << job->size;
							*packet << next_chunk;

							sw << "Wysyłam pakiet z żądaniem fragmentu " << next_chunk << "\n";
							client->send(packet);

						}

						if (job->seeds_count >= MAX_SEEDS)
							break;
					} else {
						sw << " już połączony\n";
					}
				}

				job->seeds_count_keep = job->lockCount();
			}
		}

	}

	if (active)
		QTimer::singleShot(500, this, SLOT(handleJobs()));
}

void Controller::onNewPacket(RemoteClient * client) {
	Packet * packet = client->recv();
	Packet * reply = NULL;

	if (packet == NULL)
		return;

	packet->unpack();
	packet->seek(0, 0);

	switch (packet->id) {

		// żądania od innych klientów

		case Packet::ptGet:
			reply = get(packet);
			break;

		// odpowiedzi od innych klientów

		case Packet::ptGive:
			give(packet);
			break;

		// odpowiedzi od serwera

		case Packet::ptNewID:
			newID(packet);
			break;

		case Packet::ptList:
			list(packet);
			break;

		case Packet::ptListSeeds:
			listSeeds(packet);
			break;

		default:
			break;
	}

	if (reply != NULL) {
		client->send(reply);
	}

	delete packet;
}

void Controller::onNewClient(RemoteClient * client) {
	/// @todo imlement this
	Q_UNUSED(client);
}

void Controller::onClientDied(RemoteClient * client) {
	if (client == main_server) {
		QTimer::singleShot(1000, this, SLOT(reconnect()));
		emit serverDisconnected();
	} else {
		foreach (Job * job, model.jobs())
			job->freeSeed(client->id(), true);
	}
}

} // namespace Client
